Data Editing
v6
Data editing refers to actions such as creating orders, manipulating their contents, reserving tables, registering guests, etc. Each individual action makes a small point change — for example, AddOrderItemProduct adds a dish to the order, while AddOrderItemModifier adds a modifier to the dish. Many actions can lead to inconsistent data states on their own — for instance, if a dish has mandatory modifiers, adding the dish without modifiers would violate the corresponding business rule. By combining these actions, one can add a dish with modifiers to the order. It is important that the set of actions is performed in accordance with the “all or nothing” principle and transitions the data from one consistent state to another consistent state. To ensure transactionality when changing data, the concept of editing sessions is introduced.
Editing Sessions
An editing session is somewhat similar to transactions in databases. All actions, even single ones, are performed within sessions as follows:
- An
IEditSessionsession is created by callingPluginContext.Operations.CreateEditSession. - One or more actions are performed within the session.
- The changes made are saved using the method
PluginContext.Operations.SubmitChanges.
Changes made in the second step are not visible until they are successfully saved. In the third step, either all changes are successfully recorded, or all are rolled back, and an exception is generated.
Synchronization
Access to data is synchronized using locks and revisions. During editing, objects are locked, but it is impossible to manage locks directly through the API — objects are automatically locked when changes are saved (SubmitChanges). This corresponds to the concept of optimistic locking: at the stages of creating an editing session and performing actions, objects are not locked, and when saving the changes made, a check is performed to see if they have been changed by someone else at the same time. With each change, new revisions are assigned to the objects, allowing different versions of the same object to be distinguished. Given the low competition for parallel editing of the same objects, this approach simplifies the programming interface (no lock management methods, no need to worry about their proper release) and ensures data availability (it is impossible to lock an object for a long time, it is impossible to “forget” to release a lock, in case of a plugin crash, the object will not remain in a locked state).
Some implementation features should be taken into account:
- The SyrveFront application itself can lock objects for a long time (pessimistic locking). For example, when navigating to the order editing screen, the corresponding order will be locked for at least the entire time spent on that screen (further depends on which screen the user navigates to next). During this time, it is impossible to edit the locked order through the API. It makes sense to warn waiters to lock the screen when stepping away from the stationary terminal, or to set a short auto-lock interval (default is 10 minutes). Additionally, objects can be locked briefly without user involvement (for example, when making changes via a timer).
- When locking any object, related objects are automatically locked as well. For example, when locking a banquet order, the corresponding reservation will also be locked. If any of the objects cannot be locked, the operation fails.
- For proper synchronization of data access, it is necessary to designate the main cash register in the group settings using Syrve Office. It is preferable to install the plugin that uses data editing functions on the main cash register. Installation on other terminals is allowed, but in some scenarios, there may be one-time failures in applying changes due to implementation details. Starting from V9Preview3, this restriction has been
lifted.
Performing Operations in an Uninterrupted Series
Operations (IOperationService) that require synchronization for data editing lock and then unlock objects with each call. Accordingly, when sequentially calling several operations, each of them will independently lock the data, make changes, and then unlock, allowing other requests to edit the same data to “intervene” between operation calls, resulting in some of our operations succeeding while others may fail with errors like EntityAlreadyInUseException, EntityModifiedException, and some operations may become inapplicable considering others’ edits.
For example, in a plugin that receives delivery orders from an external source (website, aggregator), it was necessary to create a delivery with an external prepayment processed. Performing all this atomically within a single editing session is impossible, as processing a payment is an irreversible operation that is performed separately, so first, we create the delivery and fill in its fields, including adding an unprocessed external prepayment (CreateEditSession, CreateDeliveryOrder, AddExternalPaymentItem, etc.), we save these changes (SubmitChanges), and then we attempt to process the prepayment (ProcessPrepay). Between these operations (SubmitChanges and ProcessPrepay), the data is unlocked and available for editing by anyone. Those who are subscribed to delivery changes may receive a notification about the creation of our new delivery and make changes to it before we process the prepayment. Writing code that is not afraid of being interrupted and can continue to work while adapting to others’ edits is labor-intensive. To facilitate the implementation of such scenarios, the ability to perform several operations in one uninterrupted series has been added.
ExecuteContinuousOperation is a special operation within which several other operations can be sequentially executed in one uninterrupted series. In the plugin code, you need to gather a series of operations into one function or lambda and pass it as a callback to the ExecuteContinuousOperation method, which will call this callback, passing it a special instance of the IOperationService designed for uninterrupted operation execution:
PluginContext.Operations.ExecuteContinuousOperation(
operations =>
{
...
operations.SubmitChanges(...);
...
operations.ProcessPrepay(...);
...
});
It should be noted that the root operation ExecuteContinuousOperation is called through the common service PluginContext.Operations, while the nested operations are called through the instance of the service obtained by the lambda (named operations in the example above). Technically, operations called through the special instance of the service work exactly the same way, but do not unlock the data afterwards, meaning that each operation requiring synchronization locks the data if they have not already been locked by previous operations, makes changes, and leaves the data locked for subsequent operations. This guarantees that no one else can “steal” the lock and “intervene,” and our subsequent operations on these same objects will not encounter EntityAlreadyInUseException. The data is unlocked when control returns from the lambda.
The following limitations should be considered:
- This function has nothing to do with atomicity or transactionality; each nested operation executes on its own and saves changes immediately. If any of the operations fail (generate an exception), previous operations will not be rolled back.
- Uninterrupted operation does not mean that no one else can do anything at all. It means that we cannot interrupt the sequential editing of a specific object. Other plugins and the SyrveFront application itself can simultaneously edit other objects that we have not touched and that are not locked by us. Data is locked as needed, so if in the middle of an uninterrupted series, after successfully performing operations on one object, we decide to make changes to another object, we may well encounter
EntityAlreadyInUseException. - Since the affected data remains locked for the entire duration of the lambda passed to
ExecuteContinuousOperation, and this may limit the work of the user, other plugins, and application functions, it is necessary to perform the series of operations as quickly as possible. Within an uninterrupted session, care should be taken when making requests to external services or hardware, avoiding the possibility of hanging for a long time, and it is better to avoid external I/O altogether. If possible, all preparatory work should be done in advance, outside of the uninterrupted session. If this is not possible, it is necessary to at least ensure reasonable waiting timeouts.
Stubs
Since the results of actions cannot be obtained until the entire session is saved, it is sometimes necessary to refer to an object that is being created but does not yet exist when performing a sequence of actions within a single session. For example, after creating an order, there needs to be a way to add a guest to it, to add a dish to that guest, and to add a modifier to that dish, even though there is as yet no order, guest, or dish. For this purpose, the concept of object stubs is introduced — certain fictitious, yet unambiguous pointers to objects. Object creation actions, such as CreateOrder or AddOrderGuest, return stubs of the type INew...Stub, which can be used within the same session instead of future objects.
Most editing methods accept such stubs as arguments, allowing both existing and new objects to be passed to them. For example, the method SetOrderType accepts IOrderStub, so it can set the type for both an already existing order (IOrder : IOrderStub) and a newly created one (INewOrderStub : IOrderStub).
However, some actions may require strictly one of the two — a new or an existing object; in such cases, the method signature will use not the base type but one of its descendants.
Expected Exceptions
Various exceptions may arise when attempting to save changes. Some of them may indicate an error in the plugin code (for example, ArgumentNullException or ArgumentOutOfRangeException), and it is not recommended to suppress such exceptions (it is better to fix the error in the code). However, some exceptions cannot be anticipated or prevented, and they should be caught and handled correctly:
EntityAlreadyInUseException— an attempt to apply changes to an object that is currently locked. You can try again later.EntityModifiedException— an attempt to apply changes to an old version of the object. This means that after the plugin read the object, it was modified by someone else. You need to read the object again and, if the planned changes are still relevant, reapply them.PermissionDeniedException— an attempt to perform actions without sufficient rights. If the user wants the plugin to be able to perform these actions, they should grant the appropriate rights using Syrve Office.- …
Syntactic Sugar
Sometimes it is necessary to perform just one action, while the explicit creation of an editing session looks cumbersome. For such cases, helper extension methods have been implemented for IOperationService, which create an editing session, perform a single action, save changes, and return the result of the action. In principle, all of this could have been written manually. It is not recommended to use these wrappers if multiple actions are expected to be executed simultaneously.